#!/usr/bin/python

from veil import extractARPDstMac
import socket, struct, sys, time, random

# Local imports 
from veil import * # for the constants
from threading import Thread
from operator import itemgetter

controlPacketCount = 0

#######################################
#    Server Port THREAD FUNCTION
#######################################

def timeout():
    global VAR
    time.sleep(int(sys.argv[4]))
    VAR = False
    return
  
def serverThread(server_socket):
    while(VAR):
        print '-----------------------------------------------------'
        client_socket, address = server_socket.accept()
        print "\n",myprintid,"Received an incoming connection from ", address
        packet = receivePacket(client_socket)
        # print 'Received packet: ',packet.encode("hex")
        if len(packet) < HEADER_LEN + 8:
            print 'Malformed packet! Packet Length:',len(packet), 'Expected:',HEADER_LEN+8
            client_socket.close()
            continue
        printPacket(packet,L)
        sys.stdout.flush
        processPacket(packet)
        client_socket.close()
    server_socket.close()
###############################################
#    Server THREAD FUNCTION ENDS HERE
###############################################

# Adds an entry to rdvStore, and also ensures that there are no duplicates
def addIfNODuplicateRDVENTRY(dist,newentry):
    global rdvStore
    for x in rdvStore[dist]:
        if x[0] == newentry[0] and x[1] == newentry[1]:
            return
    rdvStore[dist].append(newentry)
    
# Finds a logically closest gateway for a given svid    
def findThreeGateways(rdvStore,k,svid):    
    gateways = []
    dists = []
    if k not in rdvStore:
        return []
    for t in rdvStore[k]:
        if t[0] not in gateways:
            gateways.append(t[0])
            dists.append(delta(t[0], svid))
    if len(gateways) == 0:
        return []
    zipped = zip(dists, gateways)
    zipped.sort()
    gateways = [x[1] for x in zipped]
    while len(gateways) < 3:
        gateways.append('11111111111111111111111111111111')
    return gateways[:3]

#######################################
#    PROCESSPACKET FUNCTION
#######################################
def processPacket(packet):
    global routingTable, controlPacketCount, bad_node_list
    dst = getDest(packet,L)
    svid = bin2str((struct.unpack("!I", packet[8:12]))[0],L)
    packettype = getOperation(packet)
    
    if packettype == DATA:
        with open(datastatsfilename, 'a') as datastatsfile:
            if svid == myvid:
                datastatsfile.write('Sending data packet to ' + dst + '\n')
            elif dst == myvid:
                datastatsfile.write('Received data packet from ' + svid + '\n')
            else:
                datastatsfile.write('Routing data packet from ' + svid + ' to ' + dst + '\n')
    
    # forward the packet if I am not the destination
    if dst != myvid:
        routepacket(packet)
        return
    
    # I am the destination of the packet, so process it.
    print myprintid, 'Processing packet'
    printPacket(packet,L)
    payload = bin2str((struct.unpack("!I", packet[16:20]))[0],L)
    
    if packettype == RDV_DELETE:
        controlPacketCount = controlPacketCount + 1
        for i in range(len(rdvStore)):
            for j in range(len(rdvStore[i])):
                if payload in rdvStore[j]:
                    if rdvStore[i][j][0]==svid and rdvStore[i][j][1]==payload:
                        rdvStore[i].pop(j)
                        if rdvStore[i]==[]:
                           rdvStore.pop(i)
                                        
        
    if packettype == RDV_PUBLISH:
        controlPacketCount = controlPacketCount + 1
        dist = delta(myvid,payload)
        if dist not in rdvStore:
            rdvStore[dist] = []
        newentry = [svid,payload]
        addIfNODuplicateRDVENTRY(dist,newentry)
        return
        
    elif packettype == RDV_QUERY:
        controlPacketCount = controlPacketCount + 1
        k = int(payload,2)
        # search in rdv store for the logically closest gateway to reach kth distance away neighbor
        gateways = findThreeGateways(rdvStore,k,svid)
        # if found then form the reply packet and send to svid
        if gateways == [] or gateways == None or len(gateways) == 0:
            # No gateway found
            print myprintid, 'No gateway found for the rdv_query packet to reach bucket: ',k,' for node: ', svid
            return
        # create a RDV_REPLY packet and send it
        # (edited)
        replypacket = createRDV_REPLY(int(gateways[0],2),int(gateways[1],2),int(gateways[2],2),k,myvid, svid)
        routepacket(replypacket)
        return
        
    elif packettype == RDV_REPLY:
        controlPacketCount = controlPacketCount + 1
        # Fill my routing table using this new information
        gateways = struct.unpack("!III", packet[20:32])
        for gw in gateways:
            if gw == 0xFFFFFFFF: # Dummy gateway; ignore
                return
            gw_str = bin2str(gw,L)
            k = int(payload,2)
            if k not in routingTable:
                routingTable[k] = []
            known_gateways = [x[1] for x in routingTable[k]]
            if gw in known_gateways:
                continue
            if gw in bad_node_list:
            	continue
            nexthop = getNextHop(gw_str)
            if nexthop == '' or nexthop == myvid:
                print 'ERROR: no nexthop found for the gateway:',gw_str
                print 'New routing information couldnt be added! '
                return
            nh = int(pid2vid[nexthop],2)
            # Add the gateway to the routing table as non-default
            bucket_info = [nh, gw, getPrefix(myvid,k), False]
            routingTable[k].append(bucket_info)
            # Sort the routing table's k-bucket by logical distance
            dists = [delta(bin2str(x[1],L),myvid) for x in routingTable[k]]
            zipped = zip(dists, routingTable[k])
            zipped.sort()
            routingTable[k] = [x[1] for x in zipped]
            # Truncate to 3 elements
            routingTable[k] = routingTable[k][:3]
            # Set the first (closest) as the default
            routingTable[k][0][3] = True
            if len(routingTable[k]) >= 2:
                routingTable[k][1][3] = False
            if len(routingTable[k]) >= 3:
                routingTable[k][2][3] = False
            
    elif packettype == DATA:
        pass
    else:
        print myprintid, 'Unexpected Packet!!'
        
###############################################
#    SPROCESSPACKET FUNCTION ENDS HERE
###############################################
###############################################
#    getNextHop function starts here
###############################################

def getNextHop(destvid_str):
    global nextGW
    nexthop = ''
    if destvid_str == myvid:
        return myvid
    if destvid_str in vid2pid:
    	if destvid_str in vid2pid2:
	        return vid2pid[destvid_str]
    dist = delta(myvid,destvid_str)
    if dist in routingTable:
        nexthop = bin2str(routingTable[dist][0][0],L)
        nexthop = vid2pid[nexthop]
	nextGW = routingTable[dist][0][1]
    return nexthop
    
###############################################
#    getNextHop FUNCTION ENDS HERE
###############################################

###############################################
#    sendPacket function starts here
###############################################

def sendPacket(packet,nexthop):
    # connect to the nexthop
    global routingTable, pid2vid2, vid2pid2, packetDropCount, nextGW
    try:
        toSwitch = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        address = nexthop.split(':')
        toSwitch.connect((address[0],int(address[1])))
        toSwitch.send(packet)
        toSwitch.close()
    except:
        print myprintid,"Unexpected Exception: ",'received while sending packet to Switch at IP: ', address[0], 'port: ',address[1]
        printPacket(packet,L)
        
        dst = getDest(packet,L)
        
        packetDropCount = packetDropCount + 1 
        
        if getOperation(packet) == DATA:
            with open(datastatsfilename, "a") as datastatsfile:
                datastatsfile.write('Discovered node ' + address[1] + ' is down\n')
                svid = bin2str((struct.unpack("!I", packet[8:12]))[0],L)
                datastatsfile.write('Dropped packet: no route to destination.  Source: ' + svid + ' Dest: ' + dst + '\n')
        
        
        #nexthop has failed - Node Failure detected - update routing table
        global myvid
        
        bad_node = pid2vid[nexthop]
        bad_node_list.append(bad_node)
	
        length = len(myvid)
        #table = {} #lets try building a new table
        #table = routingTable
        
        nexthop_vid = pid2vid[nexthop]
        
        #Reconstructing routing table by removing the failed node
        for i in range(1,length+1):
           if i in routingTable:
        	routingTable[i] = [ j for j in routingTable[i] if bin2str( j[0], L ) != nexthop_vid]
        	if len(routingTable[i]) == 0:
        		del routingTable[i]
        
        #This is for failure notification i.e. updating the RDV point
        for dist in rdvStore:
            indexes_to_delete = []
            for idx in range(len(rdvStore[dist])):
                if rdvStore[dist][idx][0] == bad_node or rdvStore[dist][idx][1] == bad_node:
                    indexes_to_delete.append(idx)
            for idx in reversed(range(len(indexes_to_delete))):
                del rdvStore[dist][indexes_to_delete[idx]]
                    
	runningTime = time.time() - failureStartTime
	convergentFile = open('ConvergentTime.cstats','a')
	convergentFile.write('After Node ' + address[1] + ' failed. Convergent Time: ' + str(runningTime) + ' seconds ' + dst + ' \n')
        convergentFile.close()
        		
       	new_nexthop = getNextHop(dst)
        if (new_nexthop != ''):
 	    fwDir = struct.unpack("!I", packet[16:20])[0]
            fwDir = struct.pack("!I", nextGW)
            packet = packet[:16] + fwDir + packet[20:]      #reset the Forwarding Directive field
            sendPacket(packet, new_nexthop)		#to enable "fast"-failure rerouting
        else:        	 	
            #packetDropCount = packetDropCount + 1;           
            pass

        
        #what this does is it reads the routing table,
        #and forms a new table without the bad nodes
                
        #if another gateway is available - re-route
        #else just keep dropping packets
        
  
###############################################
#    sendPacket FUNCTION ENDS HERE
###############################################

###############################################
#    routepacket function starts here
###############################################

def routepacket(packet):
    global myvid, routingTable, vid2pid, myprintid, L, packetdrop_count, vid2pid2
    
    dst = getDest(packet,L)
    svid = bin2str((struct.unpack("!I", packet[8:12]))[0],L)
    
    # If it's me
    if dst == myvid:
        #print 'I am the destination!'
        processPacket(packet)
        return
    
    # Reduce the TTL for data packets
    if getOperation(packet) == DATA:
        ttl = struct.unpack("!B", packet[20])[0]
        ttl -= 1
        ttlstr = struct.pack("!B", ttl)
        packet = packet[:20] + ttlstr + packet[21:]
        # And drop if TTL is exceeded
        if ttl == 0:
            with open(datastatsfilename, 'a') as datastatsfile:
                datastatsfile.write('Dropped packet: TTL exceeded. Source: ' + svid + ' Dest: ' + dst + '\n')
                packetdrop_count = packetdrop_count + 1 #increment packet drop
            return
    
    # If I was specified as the forwarding directive, clear and continue
    if getOperation(packet) == DATA:
        fwdVid = struct.unpack("!I", packet[16:20])[0]
        if (fwdVid == int(myvid,2)):
            fwdVid = struct.pack("!I", 0xFFFFFFFF)
            packet = packet[:16] + fwdVid + packet[20:]
    
    # If it's my physical Neighbor
    if dst in vid2pid:
    	if dst in vid2pid2:
        	sendPacket(packet,vid2pid[dst])
        	return
    
    nexthop = ''
    packettype = getOperation(packet)
    while nexthop == '':
        if dst in vid2pid:
          if dst in vid2pid2:
            nexthop = vid2pid[dst]
            break
        dist = delta(myvid,dst)
        if dist == 0:
            break
        if packettype == DATA:
            fwdVid = struct.unpack("!I", packet[16:20])[0]
            if fwdVid != 0xFFFFFFFF:
                # Forwarding directive was specified.
                # Try to find the desired gateway in the routing table.
                if dist in routingTable:
                    for entry in routingTable[dist]:
                        if entry[1] == fwdVid:
                            nexthop = bin2str(entry[0],L)
                            nexthop = vid2pid[nexthop]
                            break
                break
        if dist in routingTable:
            if len(routingTable[dist]) > 0:
                nexthop = bin2str(routingTable[dist][0][0],L)
                nexthop = vid2pid[nexthop]
                break
        if (packettype != RDV_PUBLISH) and (packettype != RDV_QUERY):
            break 
        
        print myprintid,'No next hop for destination: ',dst,'dist: ', dist
        # flip the dist bit to
        dst = flipBit(dst,dist)
         
    #print 'MyVID: ', myvid, 'DEST: ', dst
    if dst != getDest(packet,L):
        # update the destination on the packet
        packet = updateDestination(packet,dst)
    if dst == myvid:
        #print "I am the destination for this RDV Q/P message:"
        printPacket(packet,L)
        processPacket(packet)
    if nexthop == '':
        print myprintid,'no route to destination' ,'MyVID: ', myvid, 'DEST: ', dst
        printPacket(packet,L)
        if packettype == DATA:
            stats_text='Dropped packet: no route to destination.  Source: ' + svid + ' Dest: ' + dst + '\n'
            with open(datastatsfilename, "a") as yes:
                yes.write(stats_text)
        return

        

    sendPacket(packet,nexthop)

###############################################
#    routepacket FUNCTION ENDS HERE
###############################################
###############################################
#    Publish function starts here
###############################################

def publish(bucket,k):
    global myvid
    dst = getRendezvousID(k,myvid)
    packet = createRDV_PUBLISH(bucket,myvid,dst)
    print myprintid, 'Publishing my neighbor', bin2str(bucket[0],L), 'to rdv:',dst
    printPacket(packet,L)
    routepacket(packet)

###############################################
#    Publish FUNCTION ENDS HERE
###############################################

###############################################
#    Query function starts here
###############################################

def query(k):
    global myvid
    dst = getRendezvousID(k,myvid)
    packet = createRDV_QUERY(k,myvid,dst)
    print myprintid, 'Querying to reach Bucket:',k, 'to rdv:',dst
    printPacket(packet,L)
    routepacket(packet)

###############################################
#    Query FUNCTION ENDS HERE
###############################################


###############################################
#    RunARount function starts here
###############################################

def runARound(round):
    global routingTable
    global vid2pid, pid2vid, mypid, myvid,L, vid2pid2, pid2vid2
    
    # start from round 2 since connectivity in round 1 is already learnt using the physical neighbors
    for i in range(2,round+1):
        # see if routing entry for this round is already available in the routing table.
        if i in routingTable:
            if len(routingTable[i]) > 0:
                #publish the information if it is already there
                for t in routingTable[i]:
                    if t[1] == int(myvid,2):
                        publish(t,i)
        # Send a query regardless of whether I have a gateway, since I want some more
        query(i)

###############################################
#    RunARound FUNCTION ENDS HERE
###############################################

if len(sys.argv) != 5:
    print '-----------------------------------------------'
    print 'Wrong number of input parameters'
    print 'Usage: ', sys.argv[0], ' <TopologyFile>', '<vid_file>', '<my_ip:my_port>', '<fail_time>'
    print 'A node will have two identifiers'
    print 'i) pid: in this project pid is mapped to IP:Port of the host so if a veil_switch is running at flute.cs.umn.edu at port 5211 than pid of this switch is = flute.cs.umn.edu:5211'
    print 'ii) vid: It is the virtual id of the switch.'
    print 'TopologyFile: It contains the adjacency list using the pids. So each line contains more than one pid(s), it is interepreted as physical neighbors of the first pid.'
    print 'vid_file: It contains the pid to vid mapping, each line here contains a two tuples (space separated) first tuple is the pid and second tuple is the corresponding vid'
    print 'enter fail time\n'
    print '-----------------------------------------------\n\n\n'
    sys.exit(0)

sleeptime = random.random()*5
print 'Sleeping :',sleeptime,' seconds!'
time.sleep(sleeptime)
topofile = sys.argv[1]
vidfile = sys.argv[2]
myport = int((sys.argv[3].split(":"))[1])
mypid = sys.argv[3]
failtime = int(sys.argv[4])	#time make the node non-functional
global VAR
VAR = True			#keeps track of timeout
global bad_node_list		#contains a list of failed nodes
bad_node_list = []
global datastatsfilename
# Learn my neighbors by reading the input adjacency list file
myneighbors = []
myvid = ''
pid2vid = {}
vid2pid = {}
pid2vid2 = {}		 
vid2pid2 = {}
routingTable = {} 
rdvStore = {} 
myprintid = ''
L = 0
datastatsfilename = str(myport) + '.stats'
datastatsfile = open(datastatsfilename, 'w')
datastatsfile.close()
controlStatsFilename = 'ControlPlane' + str(myport) + '.cstats'
controlStatsFile = open(controlStatsFilename, 'w')
controlStatsFile.close()

pid2vid2 = pid2vid.copy()
vid2pid2 = vid2pid.copy()

# Routing table is a dictionary.  It contains the values at each distance from 1 to L
# The key is the distance.  The values are lists (max. 3 elements), and each element of those lists is a list with four elements (nexthop, gateway, prefix, default)

# RDV STORE is a dictionary: it stores the list of edges with distances
# open the topo file
fin = open(topofile,'r')
# read the file line by line
line = fin.readline()
line = line.strip()
while line != '':
    if line.find(mypid) ==0:
        # this is the line which contains the neighbor list for my 
        myneighbors = (line.split(' '))[1:]
        break
    line = fin.readline()
    line = line.strip()
fin.close()

if ' ' in myneighbors:
    print 'Warning: My neighbor list contains empty pids!'
if  len(myneighbors) < 1:
    print 'Warning: My neighbor list is empty, will quit now!'
    sys.exit(0)
print 'Myneighbors: ',myneighbors    
# Learn my and myneighbor's vids
fin = open(vidfile, 'r')
line = fin.readline()
line = line.strip()
while line != '':
    tokens = line.split(' ')
    if tokens[0] in myneighbors:
        pid2vid[tokens[0]] = tokens[1]
        vid2pid[tokens[1]] = tokens[0] 
    elif tokens[0] == mypid:
        myvid = tokens[1] 
    
    line = fin.readline()
    line = line.strip()
fin.close()

# Learn L, it is the length of any vid
L = len(myvid)
myprintid = "VEIL_SWITCH: ["+mypid+'|'+myvid+']'
# Now start my serversocket to listen to the incoming packets         
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(("", myport))
server_socket.listen(5)
print myprintid, ' is now listening at port: ',myport

#Create a seperate thread to check for failure
if failtime != 0:
   failure = Thread(target = timeout)
   failure.setDaemon(True)
   failure.start()

# create a server thread to take care of incoming messages:
server = Thread(target=serverThread,args=([server_socket]))
server.setDaemon(True)
server.start()       

round = 1
# Put my physical neighbor information in the routing table in the round 1
for vid in vid2pid:
    dist = delta(vid,myvid)
    if dist not in routingTable:
        routingTable[dist] = []
    if not isDuplicateGateway(routingTable[dist], int(myvid,2)):
        if len(routingTable[dist]) == 0:
            routingTable[dist].append([int(vid,2), int(myvid,2), getPrefix(myvid,dist), True])
        elif len(routingTable[dist]) < 3:
            routingTable[dist].append([int(vid,2), int(myvid,2), getPrefix(myvid,dist), False])
startTime = time.time();
#actual_round = 1
while VAR:
    print myprintid, 'Starting Round #',round
    runARound(round)
    round = round + 1
    if round > L:
        round = L
        #actual_round = actual_round + 1
        #if actual_round > 6:
                #sys.exit(0)
                
    print '\n\t----> Routing Table at :',myvid,'|',mypid,' <----'
    for i in range(1,L+1):
        if i in routingTable:
            for j in routingTable[i]:
                                print 'Bucket #', i, 'Nexthop:',bin2str(j[0],L), 'Gateway:',bin2str(j[1],L), 'Prefix:',j[2], 'Default:',j[3]
        else:
            print 'Bucket #',i,'  --- E M P T Y --- '
    print 'RDV STORE: ', rdvStore
    print '\n --  --  --  --  -- --  --  --  --  -- --  --  --  --  -- \n'
    sys.stdout.flush
    runningTime = time.time() - startTime
    controlStatsFile = open(controlStatsFilename, 'a')
    controlStatsFile.write(str(myport) + '-' + 'ControlPacketCounts: ' + str(controlPacketCount) + ' after ' + str(runningTime) + ' seconds \n')
    controlStatsFile.close()
    controlPacketCount = 0
    time.sleep(ROUND_TIME)

controlStatsFile.close()
sys.exit(0)
